
import sys
from math import radians, cos, sin

import random
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GL.EXT.fog_coord import *
import PIL.Image as Image  # PIL
import pygame
from pygame.locals import *

fogColor = (0.8, 0.8, 0.8, 1.0)  # // Fog Colour
SCREEN_SIZE = (800, 600)


def next_p2(num):
    """ If num isn't a power of 2, will return the next higher power of two """
    rval = 1
    while (rval < num):
        rval <<= 1
    return rval

def BuildTexture(path):
    """ // Load Image And Convert To A Texture
    path can be a relative path, or a fully qualified path.
    returns False if the requested image couldn't loaded as a texture
    returns True and the texture ID if image was loaded
    """
    # Catch exception here if image file couldn't be loaded
    try:
        # Note, NYI, path specified as URL's could be access using python url lib
        # OleLoadPicturePath () supports url paths, but that capability isn't critcial to this tutorial.
        Picture = Image.open(path)
    except:
        print("Unable to open image file '%s'." % (path))
        return False, 0

    glMaxTexDim = glGetIntegerv(GL_MAX_TEXTURE_SIZE)

    WidthPixels = Picture.size[0]
    HeightPixels = Picture.size[1]

    if (WidthPixels > glMaxTexDim) or (HeightPixels > glMaxTexDim):
        # The image file is too large. Shrink it to fit within the texture dimensions
        # support by our rendering context for a GL texture.
        # Note, Feel free to experiemnt and force a resize by placing a small val into
        # glMaxTexDim (e.g. 32,64,128).
        if WidthPixels > HeightPixels:
            # Width is the domainant dimension.
            resizeWidthPixels = glMaxTexDim
            squash = float(resizeWidthPixels) / float(WidthPixels)
            resizeHeightPixels = int(HeightPixels * squash)
        else:
            resizeHeightPixels = glMaxTexDim
            squash = float(resizeHeightPixels) / float(HeightPixels)
            resizeWidthPixels = int(WidthPixels * squash)
    else:
        # // Resize Image To Closest Power Of Two
        if WidthPixels > HeightPixels:
            # Width is the domainant dimension.
            resizeWidthPixels = next_p2(WidthPixels)
            squash = float(resizeWidthPixels) / float(WidthPixels)
            resizeHeightPixels = int(HeightPixels * squash)
        else:
            resizeHeightPixels = next_p2(HeightPixels)
            squash = float(resizeHeightPixels) / float(HeightPixels)
            resizeWidthPixels = int(WidthPixels * squash)
        #
    # Resize the image to be used as a texture.
    # The Python image library provides a handy method resize ().
    # Several filtering options are available.
    # If you don't specify a filtering option will default NEAREST
    Picture = Picture.resize((resizeWidthPixels, resizeHeightPixels), Image.BICUBIC)
    lWidthPixels = next_p2(resizeWidthPixels)
    lHeightPixels = next_p2(resizeWidthPixels)
    # Now we create an image that has the padding needed
    newpicture = Image.new("RGB", (lWidthPixels, lHeightPixels), (0, 0, 0))
    newpicture.paste(Picture)

    # Create a raw string from the image data - data will be unsigned bytes
    # RGBpad, no stride (0), and first line is top of image (-1)
    pBits = newpicture.tostring("raw", "RGBX", 0, -1)

    # // Typical Texture Generation Using Data From The Bitmap
    texid = glGenTextures(1)  # // Create The Texture
    glBindTexture(GL_TEXTURE_2D, texid)  # // Bind To The Texture ID

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                    GL_LINEAR)  # // (Modify This For The Type Of Filtering You Want)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
                    GL_LINEAR)  # // (Modify This For The Type Of Filtering You Want)

    # // (Modify This If You Want Mipmaps)
    glTexImage2D(GL_TEXTURE_2D, 0, 3, lWidthPixels, lHeightPixels, 0, GL_RGBA, GL_UNSIGNED_BYTE, pBits)

    # Cleanup (python actually handles all memory for you, so this isn't necessary)
    # // Decrements IPicture Reference Count
    Picture = None
    newpicture = None
    return True, texid  # // Return True (All Good)


def Extension_Init():
    """ Determine if the fog coord extentsion is availble """

    # After calling this, we will be able to invoke glFogCoordEXT ()
    if (not glInitFogCoordEXT()):
        print("Help!  No GL_EXT_ForCoord")
        sys.exit(1)

    return True


# // Any GL Init Code & User Initialiazation Goes Here
def InitGL(Width, Height):  # We call this right after our OpenGL window is created.
    global fogColor
    global camz

    if not Extension_Init():  # // Check And Enable Fog Extension If Available
        return False  # // Return False If Extension Not Supported

    if not BuildTexture("Wall.bmp"):  # // Load The Wall Texture
        return False  # // Return False If Loading Failed

    glEnable(GL_TEXTURE_2D)  # // Enable Texture Mapping
    glClearColor(0.0, 0.0, 0.0, 0.5)  # // Black Background
    glClearDepth(1.0)  # // Depth Buffer Setup
    glDepthFunc(GL_LEQUAL)  # // The Type Of Depth Testing
    glEnable(GL_DEPTH_TEST)  # // Enable Depth Testing
    glShadeModel(GL_SMOOTH)  # // Select Smooth Shading
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)  # // Set Perspective Calculations To Most Accurate

    # // Set Up Fog
    glEnable(GL_FOG)  # // Enable Fog
    glFogi(GL_FOG_MODE, GL_LINEAR)  # // Fog Fade Is Linear
    glFogfv(GL_FOG_COLOR, fogColor)  # // Set The Fog Color
    glFogf(GL_FOG_START, 0.0)  # // Set The Fog Start
    glFogf(GL_FOG_END, 1.0)  # // Set The Fog End
    glHint(GL_FOG_HINT, GL_NICEST)  # // Per-Pixel Fog Calculation
    glFogi(GL_FOG_COORDINATE_SOURCE_EXT, GL_FOG_COORDINATE_EXT)  # // Set Fog Based On Vertice Coordinates

    return True  # // Return TRUE (Initialization Successful)


# The function called when our window is resized (which shouldn't happen if you enable fullscreen, below)
def ReSizeGLScene(Width, Height):
    if Height == 0:  # Prevent A Divide By Zero If The Window Is Too Small
        Height = 1

    glViewport(0, 0, Width, Height)  # Reset The Current Viewport And Perspective Transformation
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    # // field of view, aspect ratio, near and far
    # This will squash and stretch our objects as the window is resized.
    gluPerspective(70.0, float(Width) / float(Height), 0.1, 500.0)

    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()


def drawText(position, textString):
    font = pygame.font.Font(None, 64)
    textSurface = font.render(textString, True, (255, 255, 255, 255), (0, 0, 0, 255))
    textData = pygame.image.tostring(textSurface, "RGBA", True)
    glRasterPos3d(*position)
    glDrawPixels(textSurface.get_width(), textSurface.get_height(), GL_RGBA, GL_UNSIGNED_BYTE, textData)


def drawCube(x=0.0, y=0.0, z=0.0, s=1.0):
        """Draw a cube with texture coordinates"""
        glBegin(GL_QUADS)
        glFogCoordfEXT(0.1)
        glTexCoord2f(0.0, 0.0)
        glVertex3f(x - s, y - s, z + s)
        glTexCoord2f(1.0, 0.0)
        glVertex3f(x + s, y - s, z + s)
        glTexCoord2f(1.0, 1.0)
        glVertex3f(x + s, y + s, z + s)
        glTexCoord2f(0.0, 1.0)
        glVertex3f(x - s, y + s, z + s)
        glTexCoord2f(1.0, 0.0)
        glVertex3f(x - s, y - s, z - s)
        glTexCoord2f(1.0, 1.0)
        glVertex3f(x - s, y + s, z - s)
        glTexCoord2f(0.0, 1.0)
        glVertex3f(x + s, y + s, z - s)
        glTexCoord2f(0.0, 0.0)
        glVertex3f(x + s, y - s, z - s)
        glTexCoord2f(0.0, 1.0)
        glVertex3f(x - s, y + s, z - s)
        glTexCoord2f(0.0, 0.0)
        glVertex3f(x - s, y + s, z + s)
        glTexCoord2f(1.0, 0.0)
        glVertex3f(x + s, y + s, z + s)
        glTexCoord2f(1.0, 1.0)
        glVertex3f(x + s, y + s, z - s)
        glTexCoord2f(1.0, 1.0)
        glVertex3f(x - s, y - s, z - s)
        glTexCoord2f(0.0, 1.0)
        glVertex3f(x + s, y - s, z - s)
        glTexCoord2f(0.0, 0.0)
        glVertex3f(x + s, y - s, z + s)
        glTexCoord2f(1.0, 0.0)
        glVertex3f(x - s, y - s, z + s)
        glTexCoord2f(1.0, 0.0)
        glVertex3f(x + s, y - s, z - s)
        glTexCoord2f(1.0, 1.0)
        glVertex3f(x + s, y + s, z - s)
        glTexCoord2f(0.0, 1.0)
        glVertex3f(x + s, y + s, z + s)
        glTexCoord2f(0.0, 0.0)
        glVertex3f(x + s, y - s, z + s)
        glTexCoord2f(0.0, 0.0)
        glVertex3f(x - s, y - s, z - s)
        glTexCoord2f(1.0, 0.0)
        glVertex3f(x - s, y - s, z + s)
        glTexCoord2f(1.0, 1.0)
        glVertex3f(x - s, y + s, z + s)
        glTexCoord2f(0.0, 1.0)
        glVertex3f(x - s, y + s, z - s)
        glEnd()


class Vertex:
    x = 0.0
    y = 0.0
    z = 0.0

    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z


class Tunnel(object):
    obj = []
    xyz = []
    quadric = None

    def __init__(self):
        self.p = 10.0
        self.s = 10.0
        self.a = 45.0
        self.dx = 0.0
        self.dy = 0.0
        self.dz = 0.0
        self.sa = 0.0
        for i in range(100):
            self.gen()
        self.quadric = gluNewQuadric()  # // Create A Pointer To The Quadric Object
        gluQuadricNormals(self.quadric, GLU_SMOOTH)  # // Create Smooth Normals
        gluQuadricTexture(self.quadric, GL_TRUE)  # // Create Texture Coords

    def gen(self):
        if len(self.xyz) < 1:
            v = []
            for angle in range(int(self.sa), int(self.sa + 361), int(self.a)):
                r = radians(angle)
                x = self.s * cos(r) - self.s * sin(r)
                y = self.s * cos(r) + self.s * sin(r)
                z = 0.0
                v.append(Vertex(x, y, z))
            self.xyz.append(v)
            self.dx = 0.0
            self.dy = 0.0
            self.dz = 0.0
        else:
            self.dx += random.randrange(-20, 20, 1) / 10.0
            if self.dx < -4.0: self.dx = -4.0
            if self.dx > 4.0: self.dx = 4.0
            self.dy += random.randrange(-20, 20, 1) / 10.0
            if self.dy < -4.0: self.dy = -4.0
            if self.dy > 4.0: self.dy = 4.0
            self.sa += 5.0  # random.randrange(-50, 50, 1) / 10.0
            if self.sa < 0.0: self.sa = 0.0
            if self.sa > 360.0: self.sa = 0.0
            n = len(self.xyz) - 1
            self.dz = self.xyz[n][0].z

            v = []
            angle = self.sa
            while angle < self.sa + 360.1:
                r = radians(angle)
                x = self.s * cos(r) - self.s * sin(r) + self.dx
                y = self.s * cos(r) + self.s * sin(r) + self.dy
                z = self.dz - self.p
                v.append(Vertex(x, y, z))
                if random.randrange(0, 50) == 0:
                    self.obj.append(Vertex(x, y, z))
                angle += self.a
            self.xyz.append(v)

    def update(self, ms):
        md = ms * 100.0
        '''
        for j in range(len(self.xyz)):
            v = self.xyz[j]
            for i in range(len(v)):
                v[i].z += md
        '''
        for v in self.xyz:
            for i in range(len(v)):
                v[i].z += md

        for i in range(len(self.obj)):
            self.obj[i].z += md

        while self.xyz[0][0].z > self.p:
            self.xyz.pop(0)
            self.gen()

        while self.obj[0].z > self.p:
            self.obj.pop(0)

    def render_sector(self, n):
        n1 = n
        n2 = n + 1
        v1 = self.xyz[n1]
        v2 = self.xyz[n2]
        f1 = v1[0].z / -250.0
        if f1 < 0.0: f1 = 0.0
        if f1 > 1.0: f1 = 1.0
        f2 = v2[0].z / -250.0
        if f2 < 0.0: f2 = 0.0
        if f2 > 1.0: f2 = 1.0

        vn = len(v1) - 1
        glBegin(GL_QUADS)  # // Floor
        tx = 0.0
        ts = 1.0
        for i in range(vn):
            glFogCoordfEXT(f1)
            glTexCoord2f(tx, 0.0)
            glVertex3f(v1[i].x, v1[i].y, v1[i].z)
            glFogCoordfEXT(f1)
            glTexCoord2f(tx + ts, 0.0)
            glVertex3f(v1[i + 1].x, v1[i + 1].y, v1[i + 1].z)
            glFogCoordfEXT(f2)
            glTexCoord2f(tx + ts, 1.0)
            glVertex3f(v2[i + 1].x, v2[i + 1].y, v2[i + 1].z)
            glFogCoordfEXT(f2)
            glTexCoord2f(tx, 1.0)
            glVertex3f(v2[i].x, v2[i].y, v2[i].z)
            tx += ts
            if tx > 0.9: tx = 0.0
        try:
            glEnd()
        except:
            pass

    def render_object(self, v):
        glTranslatef(v.x, v.y, v.z)
        gluSphere(self.quadric, 4.0, 32, 32)
        glTranslatef(-v.x, -v.y, -v.z)

    def render(self):
        for i in range(len(self.xyz) - 1):
            if self.p > self.xyz[i][0].z > -250:
                self.render_sector(i)
        for v in self.obj:
            if self.p > v.z > -250:
                self.render_object(v)


def run():
    pygame.init()
    screen = pygame.display.set_mode(SCREEN_SIZE, HWSURFACE | OPENGL | DOUBLEBUF)

    InitGL(SCREEN_SIZE[0], SCREEN_SIZE[1])
    ReSizeGLScene(SCREEN_SIZE[0], SCREEN_SIZE[1])

    clock = pygame.time.Clock()

    glMaterial(GL_FRONT, GL_AMBIENT, (0.1, 0.1, 0.1, 1.0))
    glMaterial(GL_FRONT, GL_DIFFUSE, (1.0, 1.0, 1.0, 1.0))

    # This object renders the 'map'
    tunnel = Tunnel()

    movement_speed = 5.0
    eye = Vertex(0., 0., 0.)
    rot = Vertex(0., 0., 0.)
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                return
            if event.type == KEYUP and event.key == K_ESCAPE:
                return

        # Clear the screen, and z-buffer
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        time_passed = clock.tick(29)
        time_passed_seconds = time_passed / 1000.

        pressed = pygame.key.get_pressed()

        if pressed[K_LEFT]: eye.x += 0.2
        elif pressed[K_RIGHT]: eye.x -= 0.2
        if pressed[K_UP]: eye.y -= 0.2
        elif pressed[K_DOWN]: eye.y += 0.2
        if pressed[K_w]: eye.z -= 0.2
        elif pressed[K_s]: eye.z += 0.2
        if pressed[K_a]: rot.y -= 1.0
        elif pressed[K_d]: rot.y += 1.0

        glTranslatef(eye.x, eye.y, eye.z)
        # Light must be transformed as well
        glLight(GL_LIGHT0, GL_POSITION, (0, -10., -10., 0))

        glRotatef(rot.y, 0., 1., 0.)
        tunnel.update(time_passed_seconds)
        # Render the map
        tunnel.render()
        # drawText((0., 0., -1.), "Tunnel")
        # Show the screen
        glTranslatef(0., 0., -20.)
        drawCube()
        glTranslatef(0., 0., 20.)
        glRotatef(-rot.y, 0., 1., 0.)
        glTranslatef(-eye.x, -eye.y, -eye.z)
        pygame.display.flip()

if __name__ == '__main__':
    run()